/*
 * Copyright (C) 2010 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.google.doclava;

import com.google.common.collect.Iterables;
import java.util.ArrayList;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

public class Comment {
  static final Pattern LEADING_WHITESPACE = Pattern.compile("^[ \t\n\r]*(.*)$", Pattern.DOTALL);

  static final Pattern TAG_BEGIN = Pattern.compile("[\r\n][\r\n \t]*@", Pattern.DOTALL);

  static final Pattern TAG = Pattern.compile("(@[^ \t\r\n]+)[ \t\r\n]+(.*)", Pattern.DOTALL);

  static final Pattern INLINE_TAG =
      Pattern.compile("(.*?)\\{(@[^ \t\r\n\\}]+)[ \t\r\n]*(.*?)\\}", Pattern.DOTALL);

  static final Pattern FIRST_SENTENCE =
      Pattern.compile("((.*?)\\.)[ \t\r\n\\<](.*)", Pattern.DOTALL);

  private static final String[] KNOWN_TAGS =
      new String[] {"@author", "@since", "@version", "@deprecated", "@undeprecate", "@docRoot",
          "@sdkCurrent", "@inheritDoc", "@more", "@samplecode", "@sample", "@include",
          "@serial", "@com.intel.drl.spec_ref", "@ar.org.fitc.spec_ref",};

  public Comment(String text, ContainerInfo base, SourcePositionInfo sp) {
    mText = text;
    mBase = base;
    // sp now points to the end of the text, not the beginning!
    mPosition = SourcePositionInfo.findBeginning(sp, text);
  }

  private void parseRegex(String text) {
    Matcher m;

    m = LEADING_WHITESPACE.matcher(text);
    m.matches();
    text = m.group(1);

    m = TAG_BEGIN.matcher(text);

    int start = 0;
    int end;
    while (m.find()) {
      end = m.start();

      tag(text, start, end);

      start = m.end() - 1; // -1 is the @
    }
    end = text.length();
    tag(text, start, end);
  }

  private void tag(String text, int start, int end) {
    SourcePositionInfo pos = SourcePositionInfo.add(mPosition, mText, start);

    if (start >= 0 && end > 0 && (end - start) > 0) {
      text = text.substring(start, end);

      Matcher m = TAG.matcher(text);
      if (m.matches()) {
        // out of line tag
        tag(m.group(1), m.group(2), false, pos);
      } else {
        // look for inline tags
        m = INLINE_TAG.matcher(text);
        start = 0;
        while (m.find()) {
          String tagname = m.group(2);
          String tagvalue = m.group(3);
          tag(null, m.group(1), true, pos);
          tag(tagname, tagvalue, true, pos);
          start = m.end();
        }
        int len = text.length();
        if (start != len) {
          tag(null, text.substring(start), true, pos);
        }
      }
    }
  }

  private void tag(String name, String text, boolean isInline, SourcePositionInfo pos) {
    /*
     * String s = isInline ? "inline" : "outofline"; System.out.println("---> " + s + " name=[" +
     * name + "] text=[" + text + "]");
     */
    if (name == null) {
      mInlineTagsList.add(new TextTagInfo("Text", "Text", text, pos));
    } else if (name.equals("@param")) {
      mParamTagsList.add(new ParamTagInfo("@param", "@param", text, mBase, pos));
    } else if (name.equals("@see")) {
      mSeeTagsList.add(new SeeTagInfo("@see", "@see", text, mBase, pos));
    } else if (name.equals("@link") || name.equals("@linkplain")) {
      mInlineTagsList.add(new SeeTagInfo(name, "@see", text, mBase, pos));
    } else if (name.equals("@throws") || name.equals("@exception")) {
      mThrowsTagsList.add(new ThrowsTagInfo("@throws", "@throws", text, mBase, pos));
    } else if (name.equals("@return")) {
      mReturnTagsList.add(new ParsedTagInfo("@return", "@return", text, mBase, pos));
    } else if (name.equals("@deprecated")) {
      if (text.length() == 0) {
        Errors.error(Errors.MISSING_COMMENT, pos, "@deprecated tag with no explanatory comment");
        text = "No replacement.";
      }
      mDeprecatedTagsList.add(new ParsedTagInfo("@deprecated", "@deprecated", text, mBase, pos));
    } else if (name.equals("@literal")) {
      mInlineTagsList.add(new LiteralTagInfo(text, pos));
    } else if (name.equals("@code")) {
      mInlineTagsList.add(new CodeTagInfo(text, pos));
    } else if (name.equals("@hide") || name.equals("@pending") || name.equals("@doconly")) {
      // nothing
    } else if (name.equals("@attr")) {
      AttrTagInfo tag = new AttrTagInfo("@attr", "@attr", text, mBase, pos);
      mAttrTagsList.add(tag);
      Comment c = tag.description();
      if (c != null) {
        for (TagInfo t : c.tags()) {
          mInlineTagsList.add(t);
        }
      }
    } else if (name.equals("@undeprecate")) {
      mUndeprecateTagsList.add(new TextTagInfo("@undeprecate", "@undeprecate", text, pos));
    } else if (name.equals("@include") || name.equals("@sample")) {
      mInlineTagsList.add(new SampleTagInfo(name, "@include", text, mBase, pos));
    } else {
      boolean known = false;
      for (String s : KNOWN_TAGS) {
        if (s.equals(name)) {
          known = true;
          break;
        }
      }
      if (!known) {
        Errors.error(Errors.UNKNOWN_TAG, pos == null ? null : new SourcePositionInfo(pos),
            "Unknown tag: " + name);
      }
      TagInfo t = new TextTagInfo(name, name, text, pos);
      if (isInline) {
        mInlineTagsList.add(t);
      } else {
        mTagsList.add(t);
      }
    }
  }

  private void parseBriefTags() {
    int N = mInlineTagsList.size();

    // look for "@more" tag, which means that we might go past the first sentence.
    int more = -1;
    for (int i = 0; i < N; i++) {
      if (mInlineTagsList.get(i).name().equals("@more")) {
        more = i;
      }
    }
    if (more >= 0) {
      for (int i = 0; i < more; i++) {
        mBriefTagsList.add(mInlineTagsList.get(i));
      }
    } else {
      for (int i = 0; i < N; i++) {
        TagInfo t = mInlineTagsList.get(i);
        if (t.name().equals("Text")) {
          Matcher m = FIRST_SENTENCE.matcher(t.text());
          if (m.matches()) {
            String text = m.group(1);
            TagInfo firstSentenceTag = new TagInfo(t.name(), t.kind(), text, t.position());
            mBriefTagsList.add(firstSentenceTag);
            break;
          }
        }
        mBriefTagsList.add(t);

      }
    }
  }

  public List<TagInfo> tags() {
    checkInitVisibleCalled();
    return mInlineTags;
  }

  public TagInfo[] tags(String name) {
    checkInitVisibleCalled();
    ArrayList<TagInfo> results = new ArrayList<TagInfo>();
    int N = mInlineTagsList.size();
    for (int i = 0; i < N; i++) {
      TagInfo t = mInlineTagsList.get(i);
      if (t.name().equals(name)) {
        results.add(t);
      }
    }
    return results.toArray(new TagInfo[results.size()]);
  }

  public List<ParamTagInfo> paramTags() {
    checkInitVisibleCalled();
    return mParamTags;
  }

  public List<SeeTagInfo> seeTags() {
    checkInitVisibleCalled();
    return mSeeTags;
  }

  public List<ThrowsTagInfo> throwsTags() {
    checkInitVisibleCalled();
    return mThrowsTags;
  }

  public List<TagInfo> returnTags() {
    checkInitVisibleCalled();
    return mReturnTags;
  }

  public List<TagInfo> deprecatedTags() {
    checkInitVisibleCalled();
    return mDeprecatedTags;
  }

  public List<TagInfo> undeprecateTags() {
    checkInitVisibleCalled();
    return mUndeprecateTags;
  }

  public List<AttrTagInfo> attrTags() {
    checkInitVisibleCalled();
    return mAttrTags;
  }

  public List<TagInfo> briefTags() {
    checkInitVisibleCalled();
    return mBriefTags;
  }

  public boolean isHidden() {
    if (mHidden != -1) {
      return mHidden != 0;
    } else {
      if (Doclava.checkLevel(Doclava.SHOW_HIDDEN)) {
        mHidden = 0;
        return false;
      }
      boolean b = mText.indexOf("@hide") >= 0 || mText.indexOf("@pending") >= 0;
      mHidden = b ? 1 : 0;
      return b;
    }
  }

  public boolean isDocOnly() {
    if (mDocOnly != -1) {
      return mDocOnly != 0;
    } else {
      boolean b = (mText != null) && (mText.indexOf("@doconly") >= 0);
      mDocOnly = b ? 1 : 0;
      return b;
    }
  }
  
  public boolean isDeprecated() {
    if (mDeprecated != -1) {
      return mDeprecated != 0;
    } else {
      boolean b = (mText != null) && (mText.indexOf("@deprecated") >= 0);
      mDeprecated = b ? 1 : 0;
      return b;
    }
  }

  private void checkInitVisibleCalled() {
    if (!mInitialized) {
      throw new IllegalStateException("Expected initVisible() to have already been called");
    }
  }

  public void initVisible(Project project) {
    isHidden();
    isDocOnly();
    isDeprecated();

    // Don't bother parsing text if we aren't generating documentation.
    if (Doclava.parseComments()) {
      parseRegex(mText);
      parseBriefTags();
    } else {
      // Forces methods to be recognized by findOverriddenMethods in MethodInfo.
      mInlineTagsList.add(new TextTagInfo("Text", "Text", mText,
          SourcePositionInfo.add(mPosition, mText, 0)));
    }

    mText = null;
    mInitialized = true;

    for (TagInfo tagInfo : Iterables.concat(mSeeTagsList, mInlineTagsList, mReturnTagsList,
        mParamTagsList, mThrowsTagsList, mDeprecatedTagsList, mUndeprecateTagsList, mAttrTagsList,
        mBriefTagsList, mTagsList)) {
      tagInfo.initVisible(project);
    }

    mInlineTags = mInlineTagsList;
    mParamTags = mParamTagsList;
    mSeeTags = mSeeTagsList;
    mThrowsTags = mThrowsTagsList;
    mReturnTags = ParsedTagInfo.joinTags(
        mReturnTagsList.toArray(new ParsedTagInfo[mReturnTagsList.size()]));
    mDeprecatedTags = ParsedTagInfo.joinTags(
        mDeprecatedTagsList.toArray(new ParsedTagInfo[mDeprecatedTagsList.size()]));
    mUndeprecateTags = mUndeprecateTagsList;
    mAttrTags = mAttrTagsList;
    mBriefTags = mBriefTagsList;

    mParamTagsList = null;
    mSeeTagsList = null;
    mThrowsTagsList = null;
    mReturnTagsList = null;
    mDeprecatedTagsList = null;
    mUndeprecateTagsList = null;
    mAttrTagsList = null;
    mBriefTagsList = null;
  }

  boolean mInitialized;
  int mHidden = -1;
  int mDocOnly = -1;
  int mDeprecated = -1;
  String mText;
  ContainerInfo mBase;
  SourcePositionInfo mPosition;

  List<TagInfo> mInlineTags;
  List<ParamTagInfo> mParamTags;
  List<SeeTagInfo> mSeeTags;
  List<ThrowsTagInfo> mThrowsTags;
  List<TagInfo> mBriefTags;
  List<TagInfo> mReturnTags;
  List<TagInfo> mDeprecatedTags;
  List<TagInfo> mUndeprecateTags;
  List<AttrTagInfo> mAttrTags;

  ArrayList<TagInfo> mInlineTagsList = new ArrayList<TagInfo>();
  ArrayList<TagInfo> mTagsList = new ArrayList<TagInfo>();
  ArrayList<ParamTagInfo> mParamTagsList = new ArrayList<ParamTagInfo>();
  ArrayList<SeeTagInfo> mSeeTagsList = new ArrayList<SeeTagInfo>();
  ArrayList<ThrowsTagInfo> mThrowsTagsList = new ArrayList<ThrowsTagInfo>();
  ArrayList<TagInfo> mBriefTagsList = new ArrayList<TagInfo>();
  ArrayList<ParsedTagInfo> mReturnTagsList = new ArrayList<ParsedTagInfo>();
  ArrayList<ParsedTagInfo> mDeprecatedTagsList = new ArrayList<ParsedTagInfo>();
  ArrayList<TagInfo> mUndeprecateTagsList = new ArrayList<TagInfo>();
  ArrayList<AttrTagInfo> mAttrTagsList = new ArrayList<AttrTagInfo>();


}
